﻿using System;
using System.Collections.Generic;
using S7.Net;
using S7.Net.Types;
using DateTime = System.DateTime;

namespace S7PlcDataManager.S7Plc
{
    internal class S7PlcTester
    {
        private readonly Plc _plc;

        public S7PlcTester(Plc plc)
        {
            _plc = plc;
        }

        /// <summary>
        /// 根据实际地址做测试
        /// </summary>
        public void CustomTest()
        {
            #region DB1.DBW20, DB1.DBW18

            {
                // To write a ushort i don't need any cast, only unboxing must be done
                ushort val = 40000;
                _plc.Write("DB1.DBW20", val);
                var temp = _plc.Read("DB1.DBW20");
                ushort result = (ushort)temp;
                AssertAreEqual(val, result, "A ushort goes from 0 to 64512");

                // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
                // the negative sign back
                // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
                short value = -100;
                var value2 = value.ConvertToUshort();
                _plc.Write("DB1.DBW18", value2);
                var result2 = _plc.Read("DB1.DBW18");
                var result21 = (ushort)result2;
                var result22 = result21.ConvertToShort();
                //short result2 = ((ushort) _plc.Read("DB1.DBW18")).ConvertToShort();
                AssertAreEqual(value, result22, "A short goes from -32767 to 32766");
            }

            #endregion

            #region DB1.DBD26, DB1.DBD22

            {
                // To write a uint I don't need any cast, only unboxing must be done
                uint val = 1000;
                _plc.Write("DB1.DBD26", val);
                uint result = (uint)_plc.Read("DB1.DBD26");
                AssertAreEqual(val, result);

                // To write a int I need to convert it to uint, then I need to reconvert the readed value to get
                // the negative sign back
                // Depending if I'm writing on a DBD or on a LONG, I will see uint or int value in the plc
                int value = -60000;
                _plc.Write("DB1.DBD22", value);
                int result2 = ((uint)_plc.Read("DB1.DBD22")).ConvertToInt();
                AssertAreEqual(value, result2);
            }

            #endregion

            #region DB1.DBD30

            {
                // Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
                // then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
                float val2 = 1234567;
                _plc.Write("DB1.DBD30", val2.ConvertToUInt());
                float result2 = ((uint)_plc.Read("DB1.DBD30")).ConvertToFloat();
                AssertAreEqual(val2, result2);

                float val3 = 12.34567f;
                _plc.Write("DB1.DBD30", val3.ConvertToUInt());
                float result3 = ((uint)_plc.Read("DB1.DBD30")).ConvertToFloat();
                AssertAreEqual(val3, result3);
            }

            #endregion

            #region DB1.DBB16

            {
                byte value = 0x12;
                _plc.Write("DB1.DBB16", value);
                byte result = (byte)_plc.Read("DB1.DBB16");
                AssertAreEqual(value, result);
            }

            #endregion

            #region DB1.DBX8.0

            {
                _plc.Write("DB1.DBX8.0", true);
                bool result = (bool)_plc.Read("DB1.DBX8.0");
                AssertAreEqual(true, result);
            }

            #endregion

            #region MW20, MW40

            {
                // To write a ushort i don't need any cast, only unboxing must be done
                ushort val = 40000;
                _plc.Write("MW20", val);
                var temp = _plc.Read("MW20");
                ushort result = (ushort)temp;
                AssertAreEqual(val, result, "A ushort goes from 0 to 64512");

                // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
                // the negative sign back
                // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
                short value = -100;
                var value2 = value.ConvertToUshort();
                _plc.Write("MW40", value2);
                var result2 = _plc.Read("MW40");
                var result21 = (ushort)result2;
                var result22 = result21.ConvertToShort();
                AssertAreEqual(value, result22, "A short goes from -32767 to 32766");
            }

            #endregion

            #region MD30, MD50

            {
                // To write a ushort i don't need any cast, only unboxing must be done
                uint val = 30000;
                _plc.Write("MD30", val);
                var temp = _plc.Read("MD30");
                uint result = (uint)temp;
                AssertAreEqual(val, result);

                // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
                // the negative sign back
                // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
                int value = -500;
                var value2 = value.ConvertToUInt();
                _plc.Write("MD50", value2);
                var result2 = _plc.Read("MD50");
                var result21 = (uint)result2;
                var result22 = result21.ConvertToInt();
                AssertAreEqual(value, result22);
            }

            #endregion

            #region MB10

            {
                byte value = 0x34;
                _plc.Write("MB10", value);
                byte result = (byte)_plc.Read("MB10");
                AssertAreEqual(value, result);
            }

            #endregion

            #region M1.0

            {
                bool value = true;
                _plc.Write("M1.0", value);
                bool result = (bool)_plc.Read("M1.0");
                AssertAreEqual(value, result);
            }

            #endregion

            #region I1.0

            {
                bool value = true;
                _plc.Write("I1.0", value);
                bool result = (bool)_plc.Read("I1.0");
                AssertAreEqual(value, result);
            }

            #endregion
        }

        public void CommonTest()
        {
            Test01_WriteAndReadInt16Variable(); //值不对
            Test02_WriteAndReadInt32Variable();
            Test03_WriteAndReadRealVariables();
            //Received error from PLC: Object does not exist.
            //Test04_WriteAndReadInt16VariableAddress8192();
            //Received error from PLC: Object does not exist.
            //Test05_WriteAndReadInt16VariableAddress16384();
            //Received error from PLC: Object does not exist.
            //Test06_ReadMultipleBytes();
            //Received error from PLC: Address out of range.
            //Test07_WriteAndReadBooleanVariable();
            //Received error from PLC: Object does not exist.
            //Test08_WriteLargeByteArray();
            Test09_ReadWriteDouble(); //值不对
            //Received error from PLC: Object does not exist.
            //Test10_ReadWriteBytesMany();
            Test11_ReadWriteSingle(); //值不对
            //Value '229' is lower than the minimum '1970' allowed for year.
            //Test12_ReadWriteDateTimeLong();
        }

        /// <summary>
        /// Read/Write a single Int16 or UInt16 with a single request.
        /// Test that writing a UInt16 (ushort) and reading it gives the correct value.
        /// Test also that writing a Int16 (short) and reading it gives the correct value.
        /// </summary>
        private void Test01_WriteAndReadInt16Variable()
        {
            // To write a ushort i don't need any cast, only unboxing must be done
            ushort val = 40000;
            _plc.Write("DB1.DBW20", val);
            var temp = _plc.Read("DB1.DBW20");
            ushort result = (ushort)temp;
            AssertAreEqual(val, result, "A ushort goes from 0 to 64512");

            // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
            // the negative sign back
            // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
            short value = -100;
            var value2 = value.ConvertToUshort();
            _plc.Write("DB1.DBW18", value2);
            var result2 = _plc.Read("DB1.DBW18");
            var result21 = (ushort)result2;
            var result22 = result21.ConvertToShort();
            //short result2 = ((ushort) _plc.Read("DB1.DBW18")).ConvertToShort();
            AssertAreEqual(value, result22, "A short goes from -32767 to 32766");
        }

        /// <summary>
        /// Read/Write a single Int32 or UInt32 with a single request.
        /// Test that writing a UInt32 (uint) and reading it gives the correct value.
        /// Test also that writing a Int32 (int) and reading it gives the correct value.
        /// </summary>
        private void Test02_WriteAndReadInt32Variable()
        {
            // To write a uint I don't need any cast, only unboxing must be done
            uint val = 1000;
            _plc.Write("DB1.DBD26", val);
            uint result = (uint)_plc.Read("DB1.DBD26");
            AssertAreEqual(val, result);

            // To write a int I need to convert it to uint, then I need to reconvert the readed value to get
            // the negative sign back
            // Depending if I'm writing on a DBD or on a LONG, I will see uint or int value in the plc
            int value = -60000;
            _plc.Write("DB1.DBD22", value);
            int result2 = ((uint)_plc.Read("DB1.DBD22")).ConvertToInt();
            AssertAreEqual(value, result2);
        }

        /// <summary>
        /// Read/Write a single REAL with a single request.
        /// Test that writing a float and reading it gives the correct value.
        /// </summary>
        private void Test03_WriteAndReadRealVariables()
        {
            // Reading and writing a float is quite complicated, because it needs to be converted to DWord before the write,
            // then reconvert to float after the read. Float values can contain only 7 digits, so no precision is lost.
            float val2 = 1234567;
            _plc.Write("DB1.DBD30", val2.ConvertToUInt());
            float result2 = ((uint)_plc.Read("DB1.DBD30")).ConvertToFloat();
            AssertAreEqual(val2, result2);

            float val3 = 12.34567f;
            _plc.Write("DB1.DBD30", val3.ConvertToUInt());
            float result3 = ((uint)_plc.Read("DB1.DBD30")).ConvertToFloat();
            AssertAreEqual(val3, result3);
        }

        /// <summary>
        /// Tests that a read and a write on addresses bigger than 8192 are executed correctly
        /// </summary>
        private void Test04_WriteAndReadInt16VariableAddress8192()
        {
            // To write a ushort i don't need any cast, only unboxing must be done
            ushort val = 8192;
            _plc.Write("DB2.DBW8192", val);
            ushort result = (ushort)_plc.Read("DB2.DBW8192");
            AssertAreEqual(val, result, "A ushort goes from 0 to 64512");

            // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
            // the negative sign back
            // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
            short value = -8192;
            _plc.Write("DB2.DBW8192", value.ConvertToUshort());
            short result2 = ((ushort)_plc.Read("DB2.DBW8192")).ConvertToShort();
            AssertAreEqual(value, result2, "A short goes from -32767 to 32766");
        }

        /// <summary>
        /// Tests that a read and a write on addresses bigger than 8192 are executed correctly
        /// </summary>
        private void Test05_WriteAndReadInt16VariableAddress16384()
        {
            // To write a ushort i don't need any cast, only unboxing must be done
            ushort val = 16384;
            _plc.Write("DB2.DBW16384", val);
            ushort result = (ushort)_plc.Read("DB2.DBW16384");
            AssertAreEqual(val, result, "A ushort goes from 0 to 64512");

            // To write a short i need to convert it to UShort, then i need to reconvert the readed value to get
            // the negative sign back
            // Depending if i'm writing on a DWORD or on a DEC, i will see ushort or short value in the plc
            short value = -16384;
            _plc.Write("DB2.DBW16384", value.ConvertToUshort());
            short result2 = ((ushort)_plc.Read("DB2.DBW16384")).ConvertToShort();
            AssertAreEqual(value, result2, "A short goes from -32767 to 32766");
        }

        private void Test06_ReadMultipleBytes()
        {
            bool val = true;
            _plc.Write("DB2.DBX0.5", val);
            bool result = (bool)_plc.Read("DB2.DBX0.5");
            AssertAreEqual(val, result);

            ushort val1 = 16384;
            _plc.Write("DB2.DBW16384", val1);
            ushort result1 = (ushort)_plc.Read("DB2.DBW16384");
            AssertAreEqual(val1, result1, "A ushort goes from 0 to 64512");

            bool val2 = true;
            _plc.Write("DB2.DBX8192.7", val2);
            bool result2 = (bool)_plc.Read("DB2.DBX8192.7");
            AssertAreEqual(val2, result2);

            ushort val3 = 129;
            _plc.Write("DB2.DBW16", val3);
            ushort result3 = (ushort)_plc.Read("DB2.DBW16");
            AssertAreEqual(val3, result3, "A ushort goes from 0 to 64512");

            byte[] val4 = new byte[] { 0x12, 0x34 };
            _plc.Write("DB2.DBB2048", val4[0]);
            _plc.Write("DB2.DBB2049", val4[1]);
            byte result4b0 = (byte)_plc.Read("DB2.DBB2048");
            byte result4b1 = (byte)_plc.Read("DB2.DBB2049");
            AssertAreEqual(val4[0], result4b0);
            AssertAreEqual(val4[1], result4b1);

            bool val6 = true;
            _plc.Write("DB2.DBX16384.6", val6);
            bool result6 = (bool)_plc.Read("DB2.DBX16384.6");
            AssertAreEqual(val6, result6);

            var dataItems = new List<DataItem>()
            {
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 0,
                    BitAdr = 5,
                    VarType = VarType.Bit
                },
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 16384,
                    VarType = VarType.Word
                },
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 8192,
                    BitAdr = 7,
                    VarType = VarType.Bit
                },
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 16,
                    VarType = VarType.Word
                },
                // single byte
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 2048,
                    VarType = VarType.Byte
                },
                // multiple bytes
                new DataItem
                {
                    Count = 2,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 2048,
                    VarType = VarType.Byte
                },
                new DataItem
                {
                    Count = 1,
                    DataType = DataType.DataBlock,
                    DB = 2,
                    StartByteAdr = 16384,
                    BitAdr = 6,
                    VarType = VarType.Bit
                },
            };

            _plc.ReadMultipleVars(dataItems);

            AssertAreEqual(dataItems[0].Value, val);
            AssertAreEqual(dataItems[1].Value, val1);
            AssertAreEqual(dataItems[2].Value, val2);
            AssertAreEqual(dataItems[3].Value, val3);
            AssertAreEqual(dataItems[4].Value, val4[0]);
            AssertAreEqual(((byte[])dataItems[5].Value)[0], val4[0]); //dataItem[5].Value should be byte[2]
            AssertAreEqual(((byte[])dataItems[5].Value)[1], val4[1]);
            AssertAreEqual(dataItems[6].Value, val6);
        }

        /// <summary>
        /// Tests that a read and a write on addresses bigger than 8192 are executed correctly
        /// </summary>
        private void Test07_WriteAndReadBooleanVariable()
        {
            // tests when writing true/false
            _plc.Write("DB1.DBX0.0", false);
            var boolVariable = (bool)_plc.Read("DB1.DBX0.0");
            AssertIsFalse(boolVariable);

            _plc.Write("DB1.DBX0.0", true);
            var temp = _plc.Read("DB1.DBX0.0");
            boolVariable = (bool)temp;
            //boolVariable = (bool) _plc.Read("DB1.DBX0.0");
            AssertIsTrue(boolVariable);

            // tests when writing 0/1
            _plc.Write("DB1.DBX0.0", 0);
            boolVariable = (bool)_plc.Read("DB1.DBX0.0");
            AssertIsFalse(boolVariable);

            _plc.Write("DB1.DBX0.0", 1);
            boolVariable = (bool)_plc.Read("DB1.DBX0.0");
            AssertIsTrue(boolVariable);

            _plc.Write("DB1.DBX0.7", 1);
            boolVariable = (bool)_plc.Read("DB1.DBX0.7");
            AssertIsTrue(boolVariable);

            _plc.Write("DB1.DBX0.7", 0);
            boolVariable = (bool)_plc.Read("DB1.DBX0.7");
            AssertIsFalse(boolVariable);

            _plc.Write("DB1.DBX658.0", 1);
            boolVariable = (bool)_plc.Read("DB1.DBX658.0");
            AssertIsTrue(boolVariable);

            _plc.Write("DB1.DBX658.7", 1);
            boolVariable = (bool)_plc.Read("DB1.DBX658.7");
            AssertIsTrue(boolVariable);

            _plc.Write("DB2.DBX9658.0", 1);
            boolVariable = (bool)_plc.Read("DB2.DBX9658.0");
            AssertIsTrue(boolVariable);
        }

        /// <summary>
        /// Write/Read a large amount of data to test PDU max
        /// </summary>
        private void Test08_WriteLargeByteArray()
        {
            var randomEngine = new Random();
            var data = new byte[8192];
            var db = 2;
            randomEngine.NextBytes(data);

            _plc.WriteBytes(DataType.DataBlock, db, 0, data);

            var readData = _plc.ReadBytes(DataType.DataBlock, db, 0, data.Length);

            AssertAreEqual(data.Length, readData.Length);
            for (var i = 0; i < data.Length; i++)
            {
                AssertAreEqual(data[i], readData[i]);
            }
        }

        private void Test09_ReadWriteDouble()
        {
            double test_value = 55.66;
            _plc.Write(DataType.DataBlock, 1, 0, test_value);
            var result = (double)_plc.Read(DataType.DataBlock, 1, 0, VarType.LReal, 1);

            AssertAreEqual(test_value, result, "Compare Write/Read");
        }

        private void Test10_ReadWriteBytesMany()
        {
            var count = 2000;
            var dataItems = new List<byte>();
            for (int i = 0; i < count; i++)
            {
                dataItems.Add((byte)(i % 256));
            }

            _plc.WriteBytes(DataType.DataBlock, 2, 0, dataItems.ToArray());

            var res = _plc.ReadBytes(DataType.DataBlock, 2, 0, count);

            for (int x = 0; x < count; x++)
            {
                AssertAreEqual(x % 256, res[x], $"Mismatch at offset {x}, expected {x % 256}, actual {res[x]}.");
            }
        }

        private void Test11_ReadWriteSingle()
        {
            float test_value = 55.6632f;
            _plc.Write("DB1.DBD0", test_value);
            var helper = _plc.Read("DB1.DBD0");
            float test_value2 = Conversion.ConvertToFloat((uint)helper);

            AssertAreEqual(test_value, test_value2, "Compare Write/Read"); //No delta, datatype matches
        }

        private void Test12_ReadWriteDateTimeLong()
        {
            var test_value = System.DateTime.Now;
            var db = 1;
            var offset = 0;

            _plc.WriteBytes(DataType.DataBlock, db, offset, DateTimeLong.ToByteArray(test_value));
            var test_value2 = _plc.Read(DataType.DataBlock, db, offset, VarType.DateTimeLong, 1);
            if (test_value2 is DateTime)
            {
                AssertAreEqual(test_value, test_value2, "Compare DateTimeLong Write/Read");
            }
            else
            {
                throw new Exception("Test12_ReadWriteDateTimeLong");
            }
        }

        #region Assert
        private static void AssertAreEqual<T>(T t1, T t2, string msg = "") where T : IEquatable<T>
        {
            if (!t1.Equals(t2))
            {
                throw new Exception(msg);
            }
        }

        private static void AssertAreEqual(object object1, object object2, string msg = "")
        {
            if (object1 != object2)
            {
                throw new Exception(msg);
            }
        }

        private static void AssertIsTrue(bool value)
        {
            if (!value)
            {
                throw new Exception("AssertIsTrue");
            }
        }

        private static void AssertIsFalse(bool value)
        {
            if (value)
            {
                throw new Exception("AssertIsFalse");
            }
        } 
        #endregion
    }
}
